package org.uiot.protocol.codec

import groovy.transform.CompileStatic

class CachedRule implements CodecTraits, Serializable {

    private final Formula_LeftShiftBits_BIG_ENDIAN = { int i -> (length - (i + 1)) * 8 }
    private final Formula_LeftShiftBits_LITTLE_ENDIAN = { int i -> i * 8 }

    /**
     * 占位符/无效值/填充值
     */
    byte[] placeholder

    /**
     *
     */
    private final String encodingScript

    /**
     *
     */
    private final String decodingScript

    CachedRule(Map params) {
        this(params.code, params.name, params.endian, params.length, params.lengthUnit, params.type, params.encodingScript, params.decodingScript, params.placeholder)
    }

    CachedRule(String code, String name, ENDIAN endian, Integer length, LengthUnit lengthUnit, Type type, String encodingScript, String decodingScript, byte[] placeholder) {
        this.code = code
        this.name = name
        this.endian = endian
        this.length = length
        this.lengthUnit = lengthUnit
        this.type = type
        this.encodingScript = encodingScript
        this.decodingScript = decodingScript
        this.placeholder = placeholder
    }

    @CompileStatic
    private Object convertByScript(final Object original, final String scriptText) {
        if (scriptText) {
            def script = new GroovyClassLoader().parseClass(scriptText).newInstance() as Script
            script.binding.setVariable("x", original)
            script.run()
        } else {
            original
        }
    }

    @CompileStatic
    def encode(final BitOutputStream outputStream, Object value) {
        if (value != null) {
            value = convertByScript(value, encodingScript)

            if (LengthUnit.Bit == lengthUnit) {
                if (Type.Numeric == type) {
                    outputStream.write(length, value as int)
                }
            } else if (LengthUnit.Byte == lengthUnit) {
                if (Type.Numeric == type) {
                    if (ENDIAN.BIG == endian) {
                        for (int i = 1; i < (length + 1); i++) {
                            def perByteValue = (value as long) >> ((length - i) * 8) & 0xff
                            outputStream.write(8, perByteValue as int)
                        }
                    } else if (ENDIAN.LITTLE == endian) {
                        for (int i = 0; i < length; i++) {
                            def perByteValue = ((value as long) >> (i * 8) & 0xff) as int
                            outputStream.write(8, perByteValue)
                        }
                    }
                } else {
                    byte[] bytesContent

                    switch (type) {
                        case Type.StringGBK:
                            bytesContent = ((String) value).getBytes("GBK")
                            break
                        case Type.StringASCII:
                            bytesContent = ((String) value).getBytes("US-ASCII")
                            break
                        case Type.StringCompressedBCD:
                            bytesContent = BytesUtils.convertCompressedBCD2ByteArray((String) value)
                            break
                        case Type.StringUncompressedBCD:
                            bytesContent = BytesUtils.convertUncompressedBCD2ByteArray((String) value)
                            break
                    }

                    if (bytesContent.length < length) {
                        def bytesNull = [], fillLength = length - bytesContent.length
                        for (int i = 0; i < fillLength; i++) {
                            bytesNull << 0x00
                        }
                        outputStream.write(bytesNull as byte[])
                    }

                    outputStream.write(bytesContent)
                }
            }
        } else if (placeholder) {//fill value
            if (LengthUnit.Bit == lengthUnit) {
                outputStream.write(placeholder, 0, 4)
            } else if (LengthUnit.Byte == lengthUnit) {
                def bytesContent = placeholder, contentLength = bytesContent.size()

                if (contentLength > length) {
                    outputStream.write(bytesContent, 0, length)
                } else {
                    outputStream.write(bytesContent)

                    if (contentLength < length) {
                        def bytesNull = [], fillLength = length - contentLength as int
                        for (int i = 0; i < fillLength; i++) {
                            bytesNull << 0x00
                        }
                        outputStream.write(bytesNull as byte[])
                    }
                }
            }
        } else if (!placeholder) {//fill zero
            if (LengthUnit.Bit == lengthUnit) {
                outputStream.write(length, 0)
            } else if (LengthUnit.Byte == lengthUnit) {
                for (int i = 0; i < length; i++) {
                    outputStream.write(8, 0)
                }
            }
        }
    }

    @CompileStatic
    def decode(final BitSource bitSource) {
        def value

        try {
            if (LengthUnit.Bit == lengthUnit) {
                if (Type.Numeric == type) {
                    value = bitSource.readBits(length)

                    if (placeholder || BytesUtils.convertIntegerToByteArray(value) == placeholder)
                        value = null
                }
            } else if (LengthUnit.Byte == lengthUnit) {
                if (Type.Numeric == type) {
                    value = bitSource.peepBytes(length)

                    if (placeholder && value == placeholder) {
                        bitSource.confirmPeep(); value = null
                    } else {
                        def formula4LeftShiftBits
                        Class numericType = length > 4 ? Long.class : Integer.class

                        bitSource.cancelPeep()
                        Number value1 = 0.asType(numericType)

                        if (ENDIAN.BIG == endian) {
                            formula4LeftShiftBits = this.@Formula_LeftShiftBits_BIG_ENDIAN as Closure<Integer>
                        } else if (ENDIAN.LITTLE == endian) {
                            formula4LeftShiftBits = this.@Formula_LeftShiftBits_LITTLE_ENDIAN as Closure<Integer>
                        }

                        for (int i = 0; i < length; i++) {
                            Number readValue = bitSource.readBits(8).asType(numericType)

                            value1 += readValue << formula4LeftShiftBits(i)
                        }

                        value = value1
                    }
                } else {
                    value = bitSource.getBytes(length)

                    if (placeholder && value == placeholder) {
                        value = null
                    } else {
                        switch (type) {
                            case Type.StringGBK:
                                value = new String(value, "GBK").trim()
                                break
                            case Type.StringASCII:
                                value = new String(value, "US-ASCII").trim()
                                break
                            case Type.StringCompressedBCD:
                                value = BytesUtils.convertByteArrayToCompressedBCD(value)
                                break
                            case Type.StringUncompressedBCD:
                                value = BytesUtils.convertByteArrayToUncompressedBCD(value)
                                break
                        }
                    }
                }
            } else {
                throw new RuntimeException("Matching codec rule fails, check [$code] codec rules.")
            }

            return convertByScript(value, decodingScript)
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException("Decoding[ $code ] fails, does not match the actual available[${bitSource.available()}], $ex")
        }
    }
}
